#!/usr/bin/nodejs
// Dahua HTTP API Module

var net = require("net");
var events = require("events");
var util = require("util");
var request = require("request");
var NetKeepAlive = require("net-keepalive");
const md5 = require("md5");
const { v4 } = require("uuid");

var TRACE = true;
var BASEURI = false;
// 重试次数 (发送两次请求)
var RetryCount = 0;

var dahua = function (options) {
  this.options = options;
  (this.auth = {
    user: options.user,
    pass: options.pass,
    sendImmediately: false,
  }),
    events.EventEmitter.call(this);
  this.client = this.connect(options);
  TRACE = options.log;
  BASEURI = "http://" + options.host + ":" + options.port;
};

util.inherits(dahua, events.EventEmitter);

dahua.prototype.connect = function (options, headerParams) {
  var self = this;
  // var authHeader =
  //   "Basic " + new Buffer(options.user + ":" + options.pass).toString("base64");
  // Connect
  var client = net.connect(options, function () {
    RetryCount += 1;
    var header =
      "GET /cgi-bin/eventManager.cgi?action=attach&codes=[AlarmLocal,VideoMotion,VideoLoss,VideoBlind] HTTP/1.0\r\n" +
      "Host: " +
      options.host +
      ":" +
      options.port +
      "\r\n" +
      headerParams +
      "\r\n" +
      "Accept: multipart/x-mixed-replace\r\n\r\n";
    client.write(header);
    console.log("dahua.prototype.connect -> client", header);
    client.setKeepAlive(true, 1000);
    NetKeepAlive.setKeepAliveInterval(client, 5000); // sets TCP_KEEPINTVL to 5s
    NetKeepAlive.setKeepAliveProbes(client, 12); // 60s and kill the connection.
    handleConnection(self, options);
  });

  client.on("timeout", function () {
    console.log("10 mins of inactivity");
    //self.abort()
    //self.connect(options)
  });

  client.on("data", function (data) {
    handleData(self, data, options);
  });

  client.on("close", function () {
    // Try to reconnect after 30s
    // setTimeout(function () {
    //   self.connect(options);
    // }, 30000);
    handleEnd(self);
  });

  client.on("error", function (err) {
    handleError(self, err);
  });
};

dahua.prototype.ptzCommand = function (cmd, arg1, arg2, arg3, arg4) {
  var self = this;
  if (!cmd || isNaN(arg1) || isNaN(arg2) || isNaN(arg3) || isNaN(arg4)) {
    handleError(self, "INVALID PTZ COMMAND");
    return 0;
  }
  request(
    BASEURI +
      "/cgi-bin/ptz.cgi?action=start&channel=0&code=" +
      ptzcommand +
      "&arg1=" +
      arg1 +
      "&arg2=" +
      arg2 +
      "&arg3=" +
      arg3 +
      "&arg4=" +
      arg4,
    function (error, response, body) {
      if (error || response.statusCode !== 200 || body.trim() !== "OK") {
        self.emit("error", "FAILED TO ISSUE PTZ COMMAND");
      }
    }
  );
};

dahua.prototype.ptzPreset = function (preset) {
  var self = this;
  if (isNaN(preset)) handleError(self, "INVALID PTZ PRESET");
  request(
    BASEURI +
      "/cgi-bin/ptz.cgi?action=start&channel=0&code=GotoPreset&arg1=0&arg2=" +
      preset +
      "&arg3=0",
    function (error, response, body) {
      if (error || response.statusCode !== 200 || body.trim() !== "OK") {
        self.emit("error", "FAILED TO ISSUE PTZ PRESET");
      }
    }
  );
};

dahua.prototype.ptzZoom = function (multiple) {
  var self = this;
  if (isNaN(multiple)) handleError(self, "INVALID PTZ ZOOM");
  if (multiple > 0) cmd = "ZoomTele";
  if (multiple < 0) cmd = "ZoomWide";
  if (multiple === 0) return 0;

  request(
    BASEURI +
      "/cgi-bin/ptz.cgi?action=start&channel=0&code=" +
      cmd +
      "&arg1=0&arg2=" +
      multiple +
      "&arg3=0",
    function (error, response, body) {
      if (error || response.statusCode !== 200 || body.trim() !== "OK") {
        self.emit("error", "FAILED TO ISSUE PTZ ZOOM");
      }
    }
  );
};

dahua.prototype.ptzMove = function (err, success, direction, action, speed) {
  console.log("dahua.prototype.ptzMove -> action", action);
  var self = this;
  if (isNaN(speed)) {
    err("INVALID PTZ SPEED");
    // handleError(self, "INVALID PTZ SPEED")
  }
  if (action !== "start" && action !== "stop") {
    err("INVALID PTZ COMMAND");
    // handleError(self, "INVALID PTZ COMMAND");
    return 0;
  }
  if (
    direction !== "Up" &&
    direction !== "Down" &&
    direction !== "Left" &&
    direction !== "Right" &&
    direction !== "LeftUp" &&
    direction !== "RightUp" &&
    direction !== "LeftDown" &&
    direction !== "RightDown"
  ) {
    err("INVALID PTZ DIRECTION");
    // handleError(self, "INVALID PTZ DIRECTION");
    return 0;
  }
  const options = {
    url:
      BASEURI +
      "/cgi-bin/ptz.cgi?action=" +
      action +
      "&channel=0&code=" +
      direction +
      "&arg1=" +
      speed +
      "&arg2=" +
      speed +
      "&arg3=0",
    auth: this.auth,
  };
  RequestFun(options.url, options.auth)
    .then(function () {
      success(true);
    })
    .catch(function (error) {
      err(error);
    });
};

dahua.prototype.ptzStatus = function () {
  var self = this;
  request(BASEURI + "/cgi-bin/ptz.cgi?action=getStatus", function (
    error,
    response,
    body
  ) {
    if (!error && response.statusCode === 200) {
      body = body.toString().split("\r\n");
      self.emit("ptzStatus", body);
    } else {
      self.emit("error", "FAILED TO QUERY STATUS");
    }
  });
};

dahua.prototype.dayProfile = function () {
  var self = this;
  request(
    BASEURI +
      "/cgi-bin/configManager.cgi?action=setConfig&VideoInMode[0].Config[0]=1",
    function (error, response, body) {
      if (!error && response.statusCode === 200) {
        if (body === "Error") {
          // Didnt work, lets try another method for older cameras
          request(
            BASEURI +
              "/cgi-bin/configManager.cgi?action=setConfig&VideoInOptions[0].NightOptions.SwitchMode=0",
            function (error, response, body) {
              if (error || response.statusCode !== 200) {
                self.emit("error", "FAILED TO CHANGE TO DAY PROFILE");
              }
            }
          );
        }
      } else {
        self.emit("error", "FAILED TO CHANGE TO DAY PROFILE");
      }
    }
  );
};

dahua.prototype.nightProfile = function () {
  var self = this;
  request(
    BASEURI +
      "/cgi-bin/configManager.cgi?action=setConfig&VideoInMode[0].Config[0]=2",
    function (error, response, body) {
      if (!error && response.statusCode === 200) {
        if (body === "Error") {
          // Didnt work, lets try another method for older cameras
          request(
            BASEURI +
              "/cgi-bin/configManager.cgi?action=setConfig&VideoInOptions[0].NightOptions.SwitchMode=3",
            function (error, response, body) {
              if (error || response.statusCode !== 200) {
                self.emit("error", "FAILED TO CHANGE TO NIGHT PROFILE");
              }
            }
          );
        }
      } else {
        self.emit("error", "FAILED TO CHANGE TO NIGHT PROFILE");
      }
    }
  );
};

function handleData(self, data, options) {
  if (TRACE) console.log("Data: " + data);
  data = data.toString().split("\r\n");
  var i = Object.keys(data);
  i.forEach(function (id) {
    if (data[id] === "HTTP/1.1 401 Unauthorized") {
      if (RetryCount <= 3) {
        console.log("handleData -> data[1]", data[1]);
        generateAuthorization(data[1], options.user, options.pass, function (
          params
        ) {
          console.log("handleData -> headerParmas", params);
          self.connect(options, params);
        });
      }
    }
    if (data[id].startsWith("Code=")) {
      var alarm = data[id].split(";");
      var code = alarm[0].substr(5);
      var action = alarm[1].substr(7);
      var index = alarm[2].substr(6);
      self.emit("alarm", code, action, index);
    }
  });
}

function handleConnection(self, options) {
  if (TRACE) console.log("Connected to " + options.host + ":" + options.port);
  //self.socket = socket;
  self.emit("connect");
}

function handleEnd(self) {
  if (TRACE) console.log("Connection closed!");
  self.emit("end");
}

function handleError(self, err) {
  if (TRACE) console.log("Connection error: " + err);
  RetryCount = 0;
  self.emit("error", err);
}

/**
 * 根据返回的头信息 生成验证信息头
 * @param {*} data 数据
 * @param {*} username 用户名
 * @param {*} password 密码
 * @param {*} callback 回调
 */
function generateAuthorization(data, username, password, callback) {
  // 最终结果值
  const endParams = {
    realm: "",
    qop: "",
    nonce: "",
    opaque: "",
    cnonce: v4().replace(/-/gi, ""),
    nc: "00000001",
  };
  console.log("data", data);
  console.log("\n");
  const rr = data.replace("WWW-Authenticate", "Authenticate");

  rr.split(",").forEach((item) => {
    let r = item.trim().replace(/=/gi, ":");
    if (foramtParams(r, "realm:")) endParams.realm = foramtParams(r, "realm:");
    if (foramtParams(r, "qop:")) endParams.qop = foramtParams(r, "qop:");
    if (foramtParams(r, "nonce:")) endParams.nonce = foramtParams(r, "nonce:");
    if (foramtParams(r, "opaque:"))
      endParams.opaque = foramtParams(r, "opaque:");
  });
  /**
   * 获取参数时的处理
   * @param {*} data 数据
   * @param {*} val 选择搜索的值
   */
  function foramtParams(data, val) {
    if (data.indexOf(val) !== -1 || data.indexOf(val) === 0) {
      return data.substring(data.indexOf(val) + val.length).replace(/"/gi, "");
    }
    return false;
  }

  // HA1部分
  const HA1 = md5(`${username}:${endParams.realm}:${password}`);

  // HA2
  const HA2 = md5("GET:192.168.33.241:80");
  const response = md5(
    `${HA1}:${endParams.nonce}:${endParams.nc}:${endParams.cnonce}:${endParams.qop}:${HA2}`
  );
  const returnValidationHeader = `Authorization: Digest username="${username}",realm="${endParams.realm}",nonce="${endParams.nonce}",uri="192.168.33.241:80",algorithm="MD5",cnonce="${endParams.cnonce}",nc="${endParams.nc}",qop="${endParams.qop}",response="${response}"`;
  callback(returnValidationHeader);
}

/**
 * 通用request请求
 * @param {*} url 地址
 * @param {*} auth 验证
 */
function RequestFun(url, auth) {
  return new Promise(function (resolve, reject) {
    const options = {
      url,
      auth,
    };
    request(options, function (error, response, body) {
      if (error || response.statusCode !== 200 || body.trim() !== "OK") {
        resolve(true);
      } else {
        reject({
          msg: response.statusMessage,
          code: response.statusCode,
        });
      }
    });
  });
}

String.prototype.startsWith = function (str) {
  return this.slice(0, str.length) == str;
};

exports.dahua = dahua;
